---
id: quick-swipe
title: Quick Swipe
sidebar_label: Quick Swipe
description: Quick Swipe animation
keywords:
  - custom-animations
  - quick-swipe
  - carousel animation
  - carousel animation quick-swipe
  - react-native-reanimated-carousel
  - reanimated-carousel
  - reanimated carousel
  - react-native
  - snap-carousel
  - react native
  - snap carousel
  - ios
  - android
  - carousel
  - snap
  - reanimated
image:
slug: /examples/custom-animations/quick-swipe
---

{/* 

=========================================================================
=========================================================================
This page generated by /scripts/gen-pages.mjs, Don't update it manually 
=========================================================================
=========================================================================

*/}

import { Tabs } from 'nextra/components'
import { Callout } from 'nextra/components'
import Demo from '@/components/Demo'

<Callout type="info" emoji="💡">
  Check out the `quick-swipe` animation demo for the full source code [here](https://github.com/dohooo/react-native-reanimated-carousel/blob/main/example/app/app/demos/custom-animations/quick-swipe/index.tsx)
</Callout>

<Demo kind="custom-animations" name="quick-swipe" />

```tsx copy
import * as React from "react";
import type { ICarouselInstance } from "react-native-reanimated-carousel";
import Carousel from "react-native-reanimated-carousel";

import { SBItem } from "@/components/SBItem";
import { IS_WEB } from "@/constants/platform";
import { window } from "@/constants/sizes";
import * as Haptics from "expo-haptics";
import { Image, ImageSourcePropType, View, ViewStyle } from "react-native";
import { Gesture, GestureDetector } from "react-native-gesture-handler";
import Animated, {
	Easing,
	Extrapolation,
	interpolate,
	useAnimatedReaction,
	useAnimatedStyle,
	useSharedValue,
	withTiming,
} from "react-native-reanimated";
import { scheduleOnRN } from "react-native-worklets";
import { getImages } from "./images";

const data = getImages().slice(0, 68);

function Index() {
	const scrollOffsetValue = useSharedValue<number>(0);
	const ref = React.useRef<ICarouselInstance>(null);

	const baseOptions = {
		vertical: false,
		width: window.width,
		height: window.width / 2,
	} as const;

	return (
		<View
			id="carousel-component"
			dataSet={{ kind: "custom-animations", name: "quick-swipe" }}
			style={{ paddingVertical: 20 }}
		>
			<Carousel
				{...baseOptions}
				loop={false}
				enabled // Default is true, just for demo
				ref={ref}
				defaultScrollOffsetValue={scrollOffsetValue}
				testID={"xxx"}
				style={{ width: "100%" }}
				autoPlay={false}
				autoPlayInterval={1000}
				data={data}
				onConfigurePanGesture={(g) => {
					"worklet";
					g.enabled(false);
				}}
				pagingEnabled
				onSnapToItem={(index) => console.log("current index:", index)}
				windowSize={2}
				renderItem={({ index, item }) => {
					return (
						<Animated.View key={index} style={{ flex: 1 }}>
							<SBItem showIndex={false} img={item} />
						</Animated.View>
					);
				}}
			/>
			<ThumbnailPagination
				style={{ marginVertical: 9 }}
				onIndexChange={(index) => {
					ref.current?.scrollTo({ index, animated: false });
					!IS_WEB && Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
				}}
			/>
		</View>
	);
}

const ThumbnailPagination: React.FC<{
	style?: ViewStyle;
	onIndexChange?: (index: number) => void;
}> = ({ style, onIndexChange }) => {
	const [_containerWidth, setContainerWidth] = React.useState<number>(0);
	const inactiveWidth = 30;
	const activeWidth = inactiveWidth * 2;
	const itemGap = 5;
	const totalWidth =
		inactiveWidth * (data.length - 1) +
		activeWidth +
		itemGap * (data.length - 1);
	const swipeProgress = useSharedValue<number>(0);
	const activeIndex = useSharedValue<number>(0);

	const containerWidth = React.useMemo(() => {
		if (totalWidth < _containerWidth) {
			return totalWidth;
		}

		return _containerWidth;
	}, [_containerWidth, totalWidth]);

	const gesture = React.useMemo(
		() =>
			Gesture.Pan().onUpdate((event) => {
				swipeProgress.value = Math.min(Math.max(event.x, 0), containerWidth);
			}),
		[activeWidth, inactiveWidth, containerWidth],
	);

	const animStyles = useAnimatedStyle(() => {
		if (containerWidth <= 0) {
			return {};
		}

		const isOverScroll = totalWidth > containerWidth;

		if (!isOverScroll) {
			return {
				transform: [
					{
						translateX: 0,
					},
				],
			};
		}

		return {
			transform: [
				{
					translateX: -interpolate(
						swipeProgress.value,
						[0, containerWidth],
						[0, totalWidth - containerWidth],
						Extrapolation.CLAMP,
					),
				},
			],
		};
	}, [containerWidth, totalWidth, containerWidth]);

	useAnimatedReaction(
		() => activeIndex.value,
		(activeIndex) => onIndexChange && scheduleOnRN(onIndexChange, activeIndex),
		[onIndexChange],
	);

	return (
		<GestureDetector gesture={gesture}>
			<Animated.View style={{ width: "100%", overflow: "hidden" }}>
				<Animated.View
					style={[{ flexDirection: "row" }, style, animStyles]}
					onLayout={(e) => setContainerWidth(e.nativeEvent.layout.width)}
				>
					{containerWidth > 0 &&
						data.map((item, index) => {
							return (
								<ThumbnailPaginationItem
									key={index}
									source={item}
									totalItems={data.length}
									swipeProgress={swipeProgress}
									containerWidth={containerWidth}
									activeIndex={activeIndex}
									activeWidth={activeWidth}
									itemGap={itemGap}
									inactiveWidth={inactiveWidth}
									totalWidth={totalWidth}
									index={index}
									style={{ marginRight: itemGap }}
									onSwipe={() => {
										console.log(`${item} swiped`);
									}}
								/>
							);
						})}
				</Animated.View>
			</Animated.View>
		</GestureDetector>
	);
};

const ThumbnailPaginationItem: React.FC<{
	source: ImageSourcePropType;
	containerWidth: number;
	totalItems: number;
	activeIndex: Animated.SharedValue<number>;
	swipeProgress: Animated.SharedValue<number>;
	activeWidth: number;
	totalWidth: number;
	inactiveWidth: number;
	itemGap: number;
	index: number;
	onSwipe?: () => void;
	style?: ViewStyle;
}> = ({
	source,
	containerWidth,
	totalItems,
	swipeProgress,
	index,
	itemGap = 0,
	activeIndex,
	activeWidth,
	totalWidth,
	inactiveWidth,
	style,
}) => {
	const isActive = useSharedValue(0);

	useAnimatedReaction(
		() => {
			const onTheRight = index >= activeIndex.value;
			const extraWidth = onTheRight ? activeWidth - inactiveWidth : 0;

			const inputRange = [
				index * (inactiveWidth + itemGap) +
					(index === activeIndex.value ? 0 : extraWidth) -
					0.1,
				index * (inactiveWidth + itemGap) +
					(index === activeIndex.value ? 0 : extraWidth),
				(index + 1) * (inactiveWidth + itemGap) + extraWidth,
				(index + 1) * (inactiveWidth + itemGap) + extraWidth + 0.1,
			];

			return interpolate(
				(swipeProgress.value / containerWidth) * totalWidth,
				inputRange,
				[0, 1, 1, 0],
				Extrapolation.CLAMP,
			);
		},
		(_isActiveAnimVal) => {
			isActive.value = _isActiveAnimVal;
		},
		[
			containerWidth,
			totalItems,
			index,
			activeIndex,
			activeWidth,
			inactiveWidth,
			itemGap,
		],
	);

	useAnimatedReaction(
		() => {
			return isActive.value;
		},
		(isActiveVal) => {
			if (isActiveVal === 1) {
				activeIndex.value = index;
			}
		},
		[],
	);

	const animStyles = useAnimatedStyle(() => {
		const widthAnimVal = interpolate(
			isActive.value,
			[0, 1, 1, 0],
			[inactiveWidth, activeWidth, activeWidth, inactiveWidth],
			Extrapolation.CLAMP,
		);

		return {
			width: withTiming(widthAnimVal, { duration: 100, easing: Easing.bounce }),
			height: 30,
			borderRadius: 5,
			overflow: "hidden",
		};
	}, [isActive, activeWidth, inactiveWidth]);

	return (
		<Animated.View style={[animStyles, style]}>
			<Image source={source} style={{ width: "100%", height: "100%" }} />
		</Animated.View>
	);
};

export default Index;

```